import random
import sympy as sp
from functools import reduce
from operator import mul as mul_op
from .base import Generator
from ..complexity.numeric import int_complexity, rat_complexity
from ..primes import Primes

#===============================================================================
# Prime number control
#===============================================================================
PRIMES = Primes()
def next_prime(p):
    '''Returns the prime following p'''

    return PRIMES[PRIMES.index(p) + 1]

def prev_prime(p):
    '''Returns the prime before p'''

    return PRIMES[(PRIMES.index(p) or 1) - 1]

#===============================================================================
# Integer numbers generator
#===============================================================================
class IntegerGen(Generator):
    '''
    Generates integers.
    
    Examples
    --------
    
    >>> random.seed(1)
    >>> gen = IntegerGen()
    >>> [ gen.new(4, 5) for _ in range(10) ]
    [12, -12, 8, 6, 11, 7, -6, -7, -9, 11]
    '''

    def __init__(self, start_value=None, target=3):
        start_value = int(start_value if start_value is not None else 2)
        super(IntegerGen, self).__init__(start_value, target)

    def increase_propose(self):
        value = self.value
        r = random.random()

        # +1 increase in number value
        if value == 1:
            return 2
        if r > 0.9:
            return value + 1

        # Factor manipulations -------------------------------------------------
        factors = sp.factorint(value)

        # Choose a random prime p at random 
        idx = random.randrange(len(factors))
        for p, _ in zip(factors, range(idx + 1)):
            pass
        # Increment the exponent with probability 45%
        if r > 0.45:
            factors[p] += 1
        # Increment the next prime factor exponent  with probability 45%
        else:
            delta = random.randint(1, factors[p])
            factors[p] -= delta
            p = next_prime(p)
            factors[p] = factors.setdefault(p, 0) + delta
        return reduce(mul_op, (p ** e for (p, e) in factors.items()))


    def decrease_propose(self):
        value = self.value
        r = random.random()

        # -1 decrease in number value
        if value <= 2:
            return 1
        if r > 0.9:
            return value - 1

        # Factor manipulations -------------------------------------------------
        factors = sp.factorint(value)

        # Choose a random prime p at random 
        idx = random.randrange(len(factors))
        for p, _ in zip(factors, range(idx + 1)):
            pass
        # Decrement the exponent with probability 45%
        if r > 0.45:
            factors[p] -= random.randint(1, factors[p])
        # Decrement the previous prime factor exponent with probability 45%
        else:
            delta = random.randint(1, factors[p])
            factors[p] -= delta
            p = prev_prime(p)
            factors[p] = factors.setdefault(p, 0) + delta
        return reduce(mul_op, (p ** e for (p, e) in factors.items()))

    def level(self, x):
        return int_complexity(x)

#===============================================================================
# Rational numbers generator
#===============================================================================
class RationalGen(Generator):
    '''
    Generates fractions.
    
    Internally, it uses a representation of a integer as a list of exponents for
    its prime factors.
    
    Examples
    --------
    
    >>> gen = RationalGen()
    >>> [ gen.new(4, 5) for _ in range(16) ]
    [3/2, 1/4, 3/2, 3/2, 3/2, 3, 4, 4, 4, 4]
    '''
    def __init__(self, start_value=None, target=3, force_pos=True):
        self.force_pos = force_pos
        start_value = (start_value if start_value is not None else sp.Rational(1, 2))
        super(RationalGen, self).__init__(start_value, target)

        # Numer/denom generators
        numer = sp.numer(start_value)
        denom = sp.denom(start_value)
        self.numer_gen = IntegerGen(numer)
        self.denom_gen = IntegerGen(denom)

    def increase_propose(self):
        if random.random() < 0.5:
            self.numer_gen, self.denom_gen = self.denom_gen, self.numer_gen
        if random.random() < 0.5:
            self.numer_gen.increase_update()
        else:
            self.denom_gen.increase_update()
        return sp.Rational(self.numer_gen.value, self.denom_gen.value or 1)

    def decrease_propose(self):
        if random.random() < 0.5:
            self.numer_gen, self.denom_gen = self.denom_gen, self.numer_gen
        if random.random() < 0.5:
            self.numer_gen.decrease_update()
        else:
            self.denom_gen.decrease_update()
        return sp.Rational(self.numer_gen.value, self.denom_gen.value or 1)

    def level(self, x):
        return rat_complexity(x)

#===============================================================================
# Real number generator
#===============================================================================
class RealGen(RationalGen):
    def export(self, x):
        return float(sp.numer(x)) / sp.denom(x)

#===============================================================================
# Export global instances
#===============================================================================
INTEGER_GEN = IntegerGen()
RATIONAL_GEN = RationalGen()
REAL_GEN = RationalGen()

if __name__ == '__main__':
    for i in range(100):
        print(i, INTEGER_GEN.new(5))


    #import doctest
    #doctest.testmod()
